# 网络服务 http client
30 秒速记
Node发HTTP请求的底层是http.request(),返回ClientRequest,响应是另一个对象IncomingMessage- 请求是可写流,写完必须
end();响应是可读流,必须读完或destroy(),连接才能回到连接池 - 超时要自己管:
req.setTimeout()只发事件不中断,Node 15+可以传signal用AbortController取消 - 连接复用靠
Agent,Node 19起全局Agent默认开keepAlive - 重试只对幂等请求做(
GET/PUT/DELETE),带退避和总次数上限;Node 18+日常请求直接用内置fetch更省事
用 Node 写 HTTP 客户端,把请求发出去只是开始,超时、连接复用、响应读没读完、失败要不要重试,这几件事才是线上出问题的地方。 http.request() 返回的请求对象是个可写流,忘了 end() 请求就根本没发完;响应是可读流,不读完连接就回不到连接池,时间长了请求开始排队。超时也不是自动的,setTimeout 只会触发一个 timeout 事件,要自己调 destroy()。现在 Node 18 以上我一般直接用内置的 fetch,底层 http 模块留给要精细控制连接的场景。
理解「网络服务 http client」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 network 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
用一段代码把「发请求」的完整闭环串起来:成功、失败、超时、响应体消费都处理到。
const http = require('http')
function get(url, timeoutMs = 3000) {
return new Promise((resolve, reject) => {
const req = http.get(url, (res) => {
if (res.statusCode !== 200) {
res.resume() // 不关心内容也要把数据放掉,连接才能复用
return reject(new Error('HTTP ' + res.statusCode))
}
let body = ''
res.setEncoding('utf8')
res.on('data', (chunk) => (body += chunk))
res.on('end', () => resolve(body))
})
// setTimeout 只触发事件,必须自己 destroy
req.setTimeout(timeoutMs, () => req.destroy(new Error('timeout')))
// DNS 失败、连接被拒、超时 destroy 都会走到这里
req.on('error', reject)
})
}
get('http://example.com/').then(
(html) => console.log('ok', html.length),
(err) => console.log('fail', err.message), // 比如 fail getaddrinfo ENOTFOUND / fail timeout
)
几个容易漏的点:http.get() 会自动调 end(),http.request() 要自己调;error 不监听的话,一次 DNS 失败就能让进程因为未处理的 error 事件崩掉;非 200 的响应也要消费掉。Node 18 以上同样的事用 fetch 写更短:await fetch(url, { signal: AbortSignal.timeout(3000) }),但它默认不会因为 500 抛错,要自己判断 res.ok。
面试官追问
追问 1请求代码跑完打印了「请求已发送」,能说明请求成功了吗?
不能。这只说明 write / end 调用返回了,数据可能还在缓冲区里,连接可能根本没建上。成功要看 response 事件拿到状态码,失败要看 error 事件,两个都得监听,不然出错时什么都看不到。
追问 2req.setTimeout(3000) 设了,为什么请求还是挂了一分钟?
setTimeout 只负责到点触发 timeout 事件,不会帮你中断请求。要这样写才会真断:req.setTimeout(3000, () => req.destroy(new Error('timeout'))),destroy 之后 error 事件里会收到这个错误。
追问 3服务端返回 500 时我没读 body 直接 return 了,会有什么问题?
响应流没消费完,底层连接一直被占着,回不到 Agent 的连接池。错误多了连接池被占满,后面正常请求都开始排队。不关心内容也要 res.resume() 把数据放掉。
追问 4网络抖一下就失败,能不能统一加个三次重试?
只对幂等请求加,比如 GET,而且要用指数退避加随机抖动,比如 200ms、400ms、800ms。下单这种 POST 不能盲重试,服务端可能已经处理了,只是响应没回来,重试一次就重复下单。
追问 5现在还有必要用 http.request 吗,fetch 不香吗?
Node 18 起内置 fetch,日常调接口用它就行,AbortSignal.timeout(3000) 一行就能加超时。http.request 适合要控制 Agent、拿底层 socket、处理 upgrade 这类事件的场景,或者还要兼容老版本 Node。
# ClientRequest概览
30 秒速记
http.request(options, cb)返回ClientRequest:代表「正在发出去的请求」,是个可写流- 状态码、响应头、正文都在
cb拿到的res(IncomingMessage)上,不在返回值上 - 创建了不等于发出去了:带正文先
write()再end(),不带正文也要end() http.get()= 方法固定GET+ 自动end()的简写- 请求侧失败(
DNS、连接被拒、超时destroy)都走req.on('error'),不监听进程会崩
http.request() 返回的是请求对象 ClientRequest,不是响应;响应要在回调里拿。 可以把它想成一封正在写的信:你可以往里加请求头、写正文,最后 end() 才算寄出去。服务端回信之后,回调收到 res,状态码、响应头、正文都从 res 上读。这个模块和服务端那套是对称的:服务端是收 IncomingMessage、写 ServerResponse,客户端是写 ClientRequest、收 IncomingMessage。
当你调用 http.request(options) 时,会返回 ClientRequest实例,主要用来创建HTTP客户端请求。
在前面的章节里,已经对http模块的的其他方面进行了不少介绍,如http.Server、http.ServerResponse、http.IncomingMessage。
有了前面的基础,详细本文不难理解,本文更多的以例子为主。
面试官追问
追问 1同事写了 const res = http.request(opts); console.log(res.statusCode),打出来 undefined,为什么?
返回值是 ClientRequest,它上面没有 statusCode。响应是异步来的,要写在回调或者 req.on('response', res => ...) 里读 res.statusCode。
追问 2批处理脚本创建了请求、注册了回调,可一直没有任何输出,先查什么?
先看有没有调 req.end()。不调的话 Node 认为你还要继续写正文,请求一直不算发完,服务端也在等,回调永远不触发。
追问 3同一个封装要支持 GET 和带正文的 POST,用 http.get 还是 http.request?
用 http.request。它能指定方法、加请求头、write() 写正文。http.get 写死了 GET 还自动 end(),只适合最简单的拉取。
追问 4接口偶发没返回,怎么分清是请求没发完还是服务端没响应?
在几个节点打时间戳:socket 事件(拿到连接)、end() 调用、response 事件(收到响应头)、res 的 end(正文读完)。卡在哪两个点之间,问题就在哪一段。
# 简单的GET请求
30 秒速记
http.request({ method: 'GET', ... })加req.end(),或者直接http.get(url, cb)(自动end)- 回调里的
res是可读流,正文分多次data到达,等end才算完整 - 文本用
res.setEncoding('utf8')防止中文被切坏;图片、压缩包这类二进制别设编码,收Buffer - 大响应别
data += chunk全读进内存,直接
